/*NOTICE

This software was produced for the U. S. Government
under Contract No. FA8721-09-C-0002, and is
subject to the Rights in Noncommercial Computer Software
and Noncommercial Computer Software Documentation Clause
(DFARS) 252.227-7014 (JUN 1995)

(c) 2009 The MITRE Corporation. All Rights Reserved.
*/
package org.mitre.xmltransform.uml21tospmif2;

// TO DO
// 1. update uml:combinedFragment
// 2. update uml:interactionOccurrence
// 3. update location handling - execution engine support - DONE
// 4. support more node types - Pardo Nodes?
// 5. no behavior executions (no focus of controls...) - or am I going to require them??
// 6. handle local time in a method - add a standard node at the beginning of each method (exec graph) - DONE
// 7. other resource types via SAResource stereotype and GRMAcquire and GRMRelease stereotypes on messages? - gen resources and devices

import java.io.File;

import org.mitre.xmltransform.spmif.ComboStep;
import org.mitre.xmltransform.spmif.Device;
import org.mitre.xmltransform.spmif.ExecutionStep;
import org.mitre.xmltransform.spmif.ExternalReferenceStep;
import org.mitre.xmltransform.spmif.HierarchyStep;
import org.mitre.xmltransform.spmif.ReferenceStep;
import org.mitre.xmltransform.spmif.SpmifOutputDocument;
import org.mitre.xmltransform.spmif.StepState;
//import org.mitre.xmltransform.spmif.SynchronizationStep;
import org.mitre.xmltransform.transform.Transform;
import org.mitre.xmltransform.uml21.Uml21InputDocument;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

public class Uml21ToSpmif2Transform extends Transform {

	@Override
	public void initialize(File pInputFile, File pOutputFile) {
		mInput = new Uml21InputDocument(pInputFile);
		mOutput = new SpmifOutputDocument(pOutputFile);
	}
	
	public int setArguments(String[] pTransformArguments) {
		// any # of arguments is fine
		mTransformArguments = pTransformArguments;
		return SETARGS_NO_ERROR;		
	}

	public String getUsageMessage() {
		// should never get called since any number of transform-specific arguments is fine
		String result = super.getUsageMessage();
		result += " < sequence diagam 1 > [ < sequence diagram 2 > [...]]";
		return result;
	}

	public void transform() {
		if (generateOutput()) {
			mOutput.finishDocument();
			mOutput.writeOutput();
		}		
	}

	private boolean generateOutput() {
		try {
			mOutput.createProjectElement(mInput.getInputValue("/xmi:XMI/uml:Model/@name"));
			NodeList interactions = mInput.getInputNodes("//packagedElement[@xmi:type='uml:Interaction']");
			if (interactions == null) return true;
			Node n;
			for (int i = 0; i < interactions.getLength(); i++) {
				n = interactions.item(i);
				generatePerformanceScenariosForInteraction(n);
			}
			
			return true;
		} catch (Exception e) {
			e.printStackTrace();
			return false;
		}
	}
		
	private Device getLocation(Node pContext) {
		mInput.setVariable("lifelineId", mInput.getInputValue(pContext, "@covered"));
		String executionEngine = mInput.getInputValue(pContext, "ancestor::xmi:XMI/RTschedulabilityModeling_profile:SAScheduler[@base_Lifeline=$lifelineId]/@SAExecutionEngine");
		if (executionEngine == null || executionEngine.length() == 0) {
			String lifelinePropertyId = mInput.getInputValue(pContext, "parent::node()/lifeline[@xmi:id=$lifelineId]/@represents");
			mInput.setVariable("lifelinePropertyId", lifelinePropertyId);
			String classId = mInput.getInputValue(pContext, "parent::node()/ownedAttribute[@xmi:type='uml:Property'][@xmi:id=$lifelinePropertyId]/@type");
			mInput.setVariable("classId", classId);
			executionEngine = mInput.getInputValue(pContext, "ancestor::xmi:XMI/RTschedulabilityModeling_profile:SAScheduler[@base_Class=$classId]/@SAExecutionEngine");
		}
		if (executionEngine == null || executionEngine.length() == 0) {
			return null;
		}
		return new Device(executionEngine, Device.TYPE_PROCESSING, null, 1);
	}
	
	private String interpretTimeInSeconds(String pTime) {
		String time = pTime;
		int commaIndex = time.indexOf(",");
		if (commaIndex != -1) {
			int value = Integer.parseInt(time.substring(1, commaIndex));
			int firstQuoteIndex = time.indexOf("'", commaIndex + 1);
			String units = time.substring(firstQuoteIndex + 1, time.indexOf("'", firstQuoteIndex + 1));
			if (units.equalsIgnoreCase("m") || units.equalsIgnoreCase("mins")) {
				time = "" + (value*60);
			} else if (units.equalsIgnoreCase("ms")) {
				time = "" + ((float)value/1000.0);
			} else {
				time = "" + value;
			}
		}
		return time;
	}
	
	private void generatePerformanceScenariosForInteraction(Node pInteraction) {
		
		String name = mInput.getInputValue(pInteraction, "@name");
		if (mTransformArguments != null && mTransformArguments.length > 0) {
			boolean found = false;
			for (int i = 0; i < mTransformArguments.length; i++) {
				if (name.equalsIgnoreCase(mTransformArguments[i])) {
					found = true;
					break;
				}
			}
			if (!found) {
				return;
			}
		}
		NodeList nodes = mInput.getInputNodes(pInteraction, "fragment");
		
		// 1. get first MessageOccurrenceSpecification's id
		String firstMOSId = mInput.getInputValue(pInteraction, "fragment[1]/@xmi:id");
		// 2. get message which has sendEvent=id from above and get that message's @xmi:id
		String messageId = mInput.getInputValue(pInteraction, "message[@xmi:type='uml:Message'][@sendEvent='" + firstMOSId + "']/@xmi:id");
		// 3. use that xmi:id and find the /xmi:XMI/RTschedulabilityModeling_profile:SATrigger[@base_Message=that id]/@SAoccurrence
		String interarrivalTime = null;
		if (mInput.evaluateBooleanInputExpression(pInteraction, "ancestor::xmi:XMI/RTschedulabilityModeling_profile:SATrigger[@base_Message='" + messageId + "']/@SAoccurrence")) {
			interarrivalTime = mInput.getInputValue(pInteraction, "ancestor::xmi:XMI/RTschedulabilityModeling_profile:SATrigger[@base_Message='" + messageId + "']/@SAoccurrence");
		}
		if (mInput.evaluateBooleanInputExpression(pInteraction, "ancestor::xmi:XMI/PAprofile_profile:PAopenLoad[@base_Message='" + messageId + "']/@PAoccurrence")) {
			interarrivalTime = mInput.getInputValue(pInteraction, "ancestor::xmi:XMI/PAprofile_profile:PAopenLoad[@base_Message='" + messageId + "']/@PAoccurrence");
		}
		if (interarrivalTime != null && interarrivalTime.length() > 0) {
			interarrivalTime = interpretTimeInSeconds(interarrivalTime);
		} else {
			interarrivalTime = null;
		}
		String priority = null;
		if (mInput.evaluateBooleanInputExpression(pInteraction, "ancestor::xmi:XMI/PAprofile_profile:PAopenLoad[@base_Message='" + messageId + "']/@PApriority")) {
			priority = mInput.getInputValue(pInteraction, "ancestor::xmi:XMI/PAprofile_profile:PAopenLoad[@base_Message='" + messageId + "']/@PApriority");
			if (priority != null && priority.length() > 0) {
				//System.out.println("priority = " + priority);
			} else {
				priority = null;
			}
		}

		// figure out end node for naming exec graph and location
		String execGraphName = name;
		Device location = null;
		String endNodeId = mInput.getInputValue(pInteraction, 
				"message[@xmi:type='uml:Message'][@sendEvent='" + firstMOSId + "']/@receiveEvent");
		if (endNodeId != null && endNodeId.length() > 0) {
			Node endNode = mInput.getInputNode(pInteraction, 
					"fragment[@xmi:type='uml:MessageOccurrenceSpecification'][@xmi:id='" + endNodeId + "']");
			execGraphName =  buildEventOccurrenceNodeName(endNode);
			location = getLocation(endNode);
		}
		
		// create perf scenario and main exec graph
		Element perfScenarioElement = mOutput.createPerformanceScenario(name, mInput.getFileName(), interarrivalTime, "s", priority);
		Element executionGraphElement = createExecutionGraph(
				perfScenarioElement, execGraphName, true);
		StepState state = new StepState(perfScenarioElement, executionGraphElement, name, null, location);
		mOutput.registerLocation(location);
		
		generateExecutionGraph(	nodes, 0, state);
	}
	
	protected Element createExecutionGraph(Element pParent, String pName, boolean pIsMain) {
		String id = pName;
		return mOutput.createExecutionGraph(pParent, id, pName, pIsMain);
	}

	protected String buildEventOccurrenceNodeName(Node pEventOccurrence) {
		StringBuffer result = new StringBuffer();

		mInput.setVariable("fragId", mInput.getInputValue(pEventOccurrence, "@xmi:id"));
		mInput.setVariable("modelId", mInput.getInputValue("/xmi:XMI/uml:Model/@xmi:id"));
		String messageName = mInput.getInputValue(pEventOccurrence, "parent::node()/message[@receiveEvent=$fragId]/@name");
		mInput.setVariable("lifelineId", mInput.getInputValue(pEventOccurrence, "@covered"));
		String lifelineInstanceName = mInput.getInputValue(pEventOccurrence, "parent::node()/lifeline[@xmi:id=$lifelineId]/@name");
		mInput.setVariable("lifelinePropId", mInput.getInputValue(pEventOccurrence, "parent::node()/lifeline[@xmi:id=$lifelineId]/@represents"));
		mInput.setVariable("lifelineClassId", mInput.getInputValue(pEventOccurrence, "parent::node()/ownedAttribute[@xmi:id=$lifelinePropId]/@type"));
		String lifelineClassName = mInput.getInputValue(pEventOccurrence, "ancestor::uml:Model/descendant::packagedElement[@xmi:type='uml:Class'][@xmi:id=$lifelineClassId]/@name");
		String name = lifelineClassName;
		if (lifelineClassName.length() == 0) {
			name = lifelineInstanceName;
		}
		if (messageName.length() == 0) {
			//TO DO: add stereotype support - not used by E-10A
/*			if (mInput.evaluateBooleanInputExpression(pEventOccurrence, "parent::node()/message[@xmi:id=$messageId]/uml:ModelElement.stereotype")) {
				mInput.setVariable("stereotypeId", mInput.getInputValue(pEventOccurrence, "ancestor::uml:Interaction/uml:Interaction.message/uml:Message[@xmi:id=$messageId]/uml:ModelElement.stereotype/uml:Stereotype/@xmi:idref"));
				String stereotypeName = mInput.getInputValue(pEventOccurrence, "/XMI/XMI.content/uml:Model[@xmi:id=$modelId]/uml:Namespace.ownedElement/uml:Stereotype[@xmi:id=$stereotypeId]/@name");
				result.append(stereotypeName);
			} else {
				result.append("anon");
			}*/
			result.append("_");
			result.append(name);
		} else {
			result.append(messageName);
			//TO DO: argument value support - not used by E-10A
			/*NodeList messages = mInput.getInputNodes(pEventOccurrence, "ancestor::uml:Interaction/uml:Interaction.message/uml:Message[@xmi:id=$messageId]/uml:Message.argument/uml:OpaqueExpression[string-length(@body) > 0]");
			Node n;
			for (int i = 0; i < messages.getLength(); i++) {
				n = messages.item(i);
				result.append("_");
				result.append(mInput.getInputValue(n, "@body"));
			}*/
			result.append("_");
			result.append(name);
		}
		//let's put a the node number of the uml:EventOccurrence within the context of the entire XMI 
		result.append("_");
		NodeList eventOccurrences = mInput.getInputNodes(pEventOccurrence, "parent::node()/fragment[@xmi:type='uml:MessageOccurrenceSpecification']");
		Node eo;
		String xmiId;
		String nodeId = mInput.getInputValue(pEventOccurrence, "@xmi:id");
		for (int i = 0; i < eventOccurrences.getLength(); i++) {
			eo = eventOccurrences.item(i);
			xmiId = mInput.getInputValue(eo, "@xmi:id");
			if (xmiId.equals(nodeId)) {
				result.append((i+1));
				break;
			}
		}
		return result.toString();
	}

	private int generateExecutionGraph(NodeList pFragments, int pStartIndex, StepState pState) {
		Element newExecutionGraphElement;//, newPerformanceScenarioElement;

		String lifeline;
		// build hierarchy
		HierarchyStep newStep, localStep;
		NodeList combinedFragmentInteractionOperands;
		Node node, interactionOperand;
		String newExecutionGraphName;
		String id, execId, execFinishId, endFragmentId, endNodeId, msgId, operationId, eventId; //,startExecutionLocation;
		Device endNodeLocation;
		String duration;
		double localDuration = 0;
		Node endNode;
		int endIndex = pFragments.getLength();
		int index = 0;

		endFragmentId = null;
		lifeline = null;
		localStep = null;
		for (int i = pStartIndex; i < pFragments.getLength(); i++) {
			node = pFragments.item(i);
			if (mInput.evaluateBooleanInputExpression(node, "boolean(self::fragment[@xmi:type='uml:MessageOccurrenceSpecification'])")) {
				index++;
			}
			if (index > 1) {
				id = mInput.getInputValue(node, "@xmi:id");
				if (index == 2 && endFragmentId == null) {
					lifeline = mInput.getInputValue(node, "@covered");
					endFragmentId = mInput.getInputValue(node, "parent::node()/fragment[@xmi:type='uml:BehaviorExecutionSpecification'][@start='" + id + "']/@finish");					
					
					// FIGURE OUT local BasicNode
					// we have the end node (node) naming the message we are "defining"
					execId = mInput.getInputValue(node, "parent::node()/fragment[@xmi:type='uml:BehaviorExecutionSpecification'][@start='" + id + "']/@xmi:id");
					localStep = new ExecutionStep(id, execId, node, "Local_" + buildEventOccurrenceNodeName(node), mInput, mOutput);
					
					msgId = mInput.getInputValue(node, "parent::node()/message[@xmi:type='uml:Message'][@receiveEvent='" + id + "']/@xmi:id");
					duration = mInput.getInputValue(node, "ancestor::xmi:XMI/RTtimeModeling_profile:RTaction[@base_Message='" + msgId + "']/@RTduration");
					if (duration != null && duration.length() > 0) {
						localDuration = Double.parseDouble(interpretTimeInSeconds(duration));
					}

					localStep.setDuration("" + localDuration);
					pState.addParentStep(localStep);
					if (!pState.closeParentStep(execId)) {
						//System.exit(0);
					}
					
				} else {
					newStep = null;
					if (mInput.evaluateBooleanInputExpression(node, "boolean(self::fragment[@xmi:type='uml:MessageOccurrenceSpecification'])")) {
						if (mInput.getInputValue(node, "@covered").equals(lifeline)) {

							endNodeId = mInput.getInputValue(node, "parent::node()/message[@xmi:type='uml:Message'][@sendEvent='" + id + "']/@receiveEvent");
							if (endNodeId != null && endNodeId.length() > 0) {
								execId = mInput.getInputValue(node, "parent::node()/fragment[@xmi:type='uml:BehaviorExecutionSpecification'][@start='" + endNodeId + "']/@xmi:id");
								execFinishId = mInput.getInputValue(node, "parent::node()/fragment[@xmi:type='uml:BehaviorExecutionSpecification'][@start='" + endNodeId + "']/@finish");
								endNode = mInput.getInputNode(node, "parent::node()/fragment[@xmi:type='uml:MessageOccurrenceSpecification'][@xmi:id='" + endNodeId + "']");

								// get duration
								// get performance dur(L) value from RT profile
								msgId = mInput.getInputValue(node, "parent::node()/message[@xmi:type='uml:Message'][@sendEvent='" + id + "']/@xmi:id");
								duration = mInput.getInputValue(node, "ancestor::xmi:XMI/RTtimeModeling_profile:RTaction[@base_Message='" + msgId + "']/@RTduration");
								
								if (duration == null || duration.length() == 0) {
									// no duration on message, try operation
									eventId = mInput.getInputValue(node, "@event");
									operationId = mInput.getInputValue("/xmi:XMI/uml:Model//packagedElement[@xmi:type='uml:SendOperationEvent'][@xmi:id='" + eventId + "']/@operation");
									if (operationId != null && operationId.length() > 0) {
										duration = mInput.getInputValue(node, "ancestor::xmi:XMI/RTtimeModeling_profile:RTaction[@base_Operation='" + operationId + "']/@RTduration");
									}
								}
								if (duration != null && duration.length() > 0) {
									duration = interpretTimeInSeconds(duration);
								} else {
									duration = null;
								}
								if (duration != null && localDuration > 0) {
									localDuration -= Double.parseDouble(duration);
								}
								
								// get locations
								//startExecutionLocation = getLocation(node);
								endNodeLocation = getLocation(endNode);									
								mOutput.registerLocation(endNodeLocation);
								if (execFinishId != null && execFinishId.length() > 0 && !endNodeId.equals(execFinishId)) { // && duration == null) {
									// we create a ReferenceStep and a sep exec graph
									newExecutionGraphName = buildEventOccurrenceNodeName(endNode);
									newStep = new ReferenceStep(id, execId, endNode, newExecutionGraphName, mInput, mOutput);
	
									//newExecutionGraphName = mInput.buildNodeName(endNode, null);
									newExecutionGraphElement = createExecutionGraph(pState.getPerformanceScenarioElement(), newExecutionGraphName, false); 
									generateExecutionGraph(	pFragments,
															i, 
															new StepState(	pState.getPerformanceScenarioElement(), newExecutionGraphElement,
																			newExecutionGraphName, pState, endNodeLocation));
									
									((ReferenceStep)newStep).setEG(newExecutionGraphName, newExecutionGraphName);
								} else {
									newStep = new ExecutionStep(id, execId, endNode, buildEventOccurrenceNodeName(endNode), mInput, mOutput);
									newStep.setLocation(endNodeLocation);
									newStep.setDuration(duration);
								}
								pState.addParentStep(newStep);
								if (!pState.closeParentStep(execId)) {
								}
							}
						}
						if (endFragmentId != null && id.equals(endFragmentId)) {
							localStep.setDuration("" + localDuration);
							break;
						}
					} else if (mInput.evaluateBooleanInputExpression(node, "boolean(self::fragment[@xmi:type='uml:CombinedFragment'])")) {
						newStep = new ComboStep(id, null, node, buildEventOccurrenceNodeName(node), mInput, mOutput);
						pState.addChildStep(newStep);
						// more execution graphs?
						combinedFragmentInteractionOperands = mInput.getInputNodes(node, "uml:CombinedFragment.operand/uml:InteractionOperand");
						for (int j = 0; j < combinedFragmentInteractionOperands.getLength(); j++) {
							interactionOperand = combinedFragmentInteractionOperands.item(j);
							newExecutionGraphName = mInput.buildNodeName(interactionOperand, null);
							newExecutionGraphElement = createExecutionGraph(pState.getPerformanceScenarioElement(), newExecutionGraphName, false); 
							generateExecutionGraph(	mInput.getInputNodes(interactionOperand, "uml:InteractionOperand.fragment/*"),
													0, 
													new StepState(pState.getPerformanceScenarioElement(), newExecutionGraphElement, newExecutionGraphName, pState, pState.getLocation()));
						}
					} else if (mInput.evaluateBooleanInputExpression(node, "boolean(self::uml:InteractionOccurrence)")) {
						newStep = new ExternalReferenceStep(id, null, node, mInput, mOutput);
						pState.addChildStep(newStep);
					}
					pState.addToStepMap(id, newStep);
				}
			}
		}
		
		if (!pState.isValidEndState()) {
			throw new IllegalArgumentException("Failed to close all parent steps.");
		}
		
		// start node
		if (pState.getNumberOfRootSteps() > 0) {
			HierarchyStep startStep = pState.getRootStep(0);
			pState.getExecutionGraphElement().setAttribute("StartNode", startStep.getStartNodeId());
		} else {
			pState.getExecutionGraphElement().setAttribute("StartNode", "");
		}
				
		// nodes
		HierarchyStep step;
		for (int i = 0; i < pState.getNumberOfRootSteps(); i++) {
			step = pState.getRootStep(i);
			step.generateStep(pState.getExecutionGraphElement(), pState.getLocation());
		}
		
		// arcs
		String fromNode = null;
		for (int i = 0; i < pState.getNumberOfRootSteps(); i++) {
			step = pState.getRootStep(i);
			fromNode = step.generateArcs(pState.getExecutionGraphElement(), fromNode);
		}
		return endIndex;
	}
	
	private String[] mTransformArguments;
	private Uml21InputDocument mInput;
	private SpmifOutputDocument mOutput;
}
